import { ProjectService } from "@/service/projects-api"
import { ChromeWorker } from "@/utils/chrome-worker"
import { ComUtils } from "@/utils/Com"
import { StrUtils } from "@/utils/str"
import JSZip from "jszip"
import { getResourceSuffix } from "./com"
import { ScriptResource } from "./resource"
import base64 from 'base64-js'

const MENUS_PREFIX = 'menu'

export class UserScriptWorker {
    static GM_CACHE_PREFIX = 'gm-'
    bgWorker: ChromeWorker
    static MAIN_SCRIPT_PREFIX = 'script-dog-'
    static GLOBAL_FUNC_PREFIX = 'SD_'
    static MAX_RETRY = 3
    private _subscriptMap: ScriptsubscriptMap[] = []
    constructor(w: ChromeWorker) {
        this.bgWorker = w;
        this.initEvents();
        this._subscriptMap = [];
        this.registOrUpdateScripts();
    }
    static init(w: ChromeWorker) {
        return new UserScriptWorker(w)
    }

    public get subscriptMap() {
        return this._subscriptMap;
    }

    public get MENUS_PREFIX() {
        return MENUS_PREFIX
    }

    private initEvents() {
        chrome.runtime.onUserScriptMessage?.addListener((message: ChromeMessage, sender, sendResponse) => {
            const func: any = Reflect.get(this, `_${message.type}`)
            if (func && typeof func === 'function') {
                func.call(this, message, sender.tab).then((res: any) => {
                    sendResponse(res);
                })
            } else {
                sendResponse('unknown message type')
            }
            return true
        })

        //右键菜单
        chrome.contextMenus.onClicked.addListener((info) => {
            const id = String(info.menuItemId);
            const index = this.subscriptMap.findIndex(sm => sm.id === id)
            if (index > -1) {
                ComUtils.sendMessageForTabs({
                    type: id,
                    data: undefined
                })
            }
        });
    }

    getChromeResponse(state: boolean, data: any, message?: any) {
        const res: ChromeResponse = {
            state,
            data,
            message
        }

        return res
    }
    private getTemplates(templates: { [key: string]: string }) {
        const ar: string[] = []
        for (const key in templates) {
            const eventDispatch = `Dog.registerEvents();`
            const code = `const get_${key} = (args={}) => {${eventDispatch}\n return \`${templates[key]}\`};window.get_${key}=get_${key};Dog.get_${key}=get_${key};`;
            ar.push(code)
        }
        return ar
    }

    public getScriptSubscriptMap(id: string): ScriptsubscriptMap[] {
        const mid = MENUS_PREFIX + '_' + id;
        const subscriptMap = this.subscriptMap.filter(sm => {
            return sm.id.startsWith(mid)
        });
        return subscriptMap
    }

    public dispatchMenuEvents(mid: string) {
        const index = this.subscriptMap.findIndex(sm => sm.id === mid)
        if (index > -1) {
            ComUtils.sendMessageForTabs({
                type: mid,
                data: undefined
            })
        }
    }

    async getRuntime(script: ProjectItem) {
        const info = {
            version: script.version,
            uuid: script.id,
            description: script.description,
            name: script.name,
            matches: script.matches,
            author: script.author,
            dogVersion: chrome.runtime.getManifest().version
        }
        const module: string[] = []

        //资源与缓存
        const resourc = new ScriptResource(script.id);
        const resourcs = script.resources || [];
        const runtimeResources: ScriptResourceInfo[] = []
        for (const rec of resourcs) {

            if (script.useDog === false && script.useGM === false) {
                break
            }

            const [recName, recUrl] = rec.split(/\s/);
            if (!recName || !recUrl) {
                script.error = 'resourcs format error: ' + rec;
                throw Error(script.error);
            }

            const res = await ComUtils.fetchWithTimeout(recUrl, {
                method: 'GET',
            }, 6000)
                .then(res => {
                    if (res.ok) {

                        return {
                            data: res.arrayBuffer(),
                            type: res.headers.get('content-type')
                        }
                    }
                }).catch(e => console.log(e))
            if (!res) {
                script.error = 'fetch error: ' + recUrl;
                throw Error(script.error);
            }
            const buff = new Uint8Array(await res.data)
            runtimeResources.push({
                id: ProjectService.getId(),
                sid: script.id,
                key: recName,
                resourceType: res.type || 'text/plain',
                type: 'resource',
                data: base64.fromByteArray(buff),
            })

        }
        module.push(await resourc.getRuntime(runtimeResources));

        if (script.useDog !== false) {

            const dog = ComUtils.getResourceUrl('/scripts/runtime.js')
            const dogCode = await fetch(dog).then(res => res.text())
            module.push(dogCode.replace('\n', ''))

            // 添加模板
            const tmps = this.getTemplates(script.templates)
            if (tmps.length > 0) {
                module.push(tmps.join(';'))
            }

            //初始化
            module.push(`Dog.info = ${JSON.stringify(info)};`);
            module.push('Dog.init();');
            module.push('Dog.compatibleGM_x();')
        }

        // 处理依赖库
        const requires = script.requires || [];
        const libs: string[] = []
        for (const lib of requires) {

            const libCode = await ComUtils.fetchWithTimeout(lib, {
                method: 'GET',
            }, 6000)
                .then(res => {
                    if (res.ok) {
                        return res.text()
                    }
                }).catch(e => console.log(e))
            if (!libCode) {
                script.error = 'fetch error: ' + lib;
                throw Error(script.error);
            }
            libs.push(libCode)
        }

        return `${module.join(';')}\n${libs.join('\n')}`;
    }
    /**
     * 生成代码
     * @param script 
     * @returns 
     */
    async getCode(script: ProjectItem) {
        const coreCode = `${script.code}\n typeof main === 'function' && main();`
        return `(function(){\n${coreCode}\n})();`

    }

    private async getRegisterValidScripts(script: ProjectItem) {
        const runtimeCode = await this.getRuntime(script).catch((e: Error) => e);
        const userCode = await this.getCode(script);
        if (typeof runtimeCode !== 'string') {
            // 生成代码失败
            script.error = runtimeCode.message;
            ProjectService.save(script);
            throw Error(runtimeCode.message)
        } else {
            script.error = undefined
            ProjectService.save(script)
        }

        const temp: chrome.userScripts.RegisteredUserScript = {
            id: script.id,
            matches: script.matches.length > 0 ? script.matches : ['<all_urls>'],
            includeGlobs: script.include,
            excludeGlobs: script.exclude,
            runAt: script.runAt,
            js: [
                {
                    code: runtimeCode
                },
                {
                    code: userCode
                }
            ]
        }

        return temp
    }

    /**
     * 注册或更新用户脚本
     * @returns 
     */
    async registOrUpdateScripts() {
        if (typeof chrome.userScripts === 'undefined') {
            return this.getChromeResponse(false, undefined, 'need developer mode')
        }
        const scripts = await ProjectService.list();

        //取消注册的脚本
        await chrome.userScripts.unregister();

        await ComUtils.waitTime();

        const tempScripts = scripts.filter(s => s.enabled && s.code.trim() && s.type === 'content');

        for (const s of tempScripts) {

            s.lastTime = Date.now();

            const vscript = await this.getRegisterValidScripts(s).catch(() => undefined);
            if (!vscript) {
                continue;
            }

            // 一个一个的注册脚本，失败则跳过
            try {
                await chrome.userScripts.register([vscript])
            } catch (error: any) {
                s.error = error.message || 'regist scripts error';
                ProjectService.save(s);
                continue
            }

        }

        return this.getChromeResponse(true, undefined)

    }

    async updateScript(id: string) {
        const script = await ProjectService.one(id);
        if (!script) {
            return this.getChromeResponse(false, undefined)
        }
        const temp = await chrome.userScripts.getScripts({
            ids: [id]
        })
        if (temp && !script.enabled) {
            await chrome.userScripts.unregister({
                ids: [id]
            })
            return this.getChromeResponse(true, undefined)
        }
        const vscript = await this.getRegisterValidScripts(script).catch(() => undefined);
        if (!vscript) {
            return this.getChromeResponse(false, undefined)
        }

        try {
            if (temp.length > 0) {
                await chrome.userScripts.update([
                    vscript,
                ])
            } else {
                await chrome.userScripts.register([
                    vscript,
                ])
            }
            return this.getChromeResponse(true, undefined)
        } catch (error: any) {
            script.error = error.message || 'unknown error';
            ProjectService.save(script);
            return this.getChromeResponse(false, undefined, error)
        }

    }

    async _openInTab(message: ChromeMessage) {
        const { url } = message.data
        await chrome.tabs.create({ url })
        return this.getChromeResponse(true, undefined)
    }

    async _notification(message: ChromeMessage) {
        const { title, text, image } = message.data
        const defaultIcon = ComUtils.getResourceUrl('/logo.png')
        chrome.notifications.create({
            message: text || '',
            iconUrl: image || defaultIcon,
            title: title || 'script dog notification',
            type: image ? 'image' : 'basic'
        })
        return this.getChromeResponse(true, undefined)
    }

    /**
     * 不添加真实菜单
     * @param message 
     * @returns 
     */
    async _registerMenuCommand(message: ChromeMessage) {
        const { mid, caption } = message.data
        const id = MENUS_PREFIX + '_' + mid;
        const index = this.subscriptMap.findIndex(sm => sm.id === id)
        if (index === -1) {
            this.subscriptMap.push({
                id,
                title: caption
            });
        }

        return this.getChromeResponse(true, undefined)
    }

    async _unregisterMenuCommand(message: ChromeMessage) {
        const id = MENUS_PREFIX + '_' + message.data;
        const index = this.subscriptMap.findIndex(s => s.id === id);
        this.subscriptMap.splice(index, 1);
        return this.getChromeResponse(true, undefined)
    }

    async _clearMenus(message: ChromeMessage) {
        const id = MENUS_PREFIX + '_' + message.data;
        this._subscriptMap = this.subscriptMap.filter(sm => {
            return !sm.id.includes(id)
        })
        return this.getChromeResponse(true, undefined)
    }

    async _xmlHttpRequest(message: ChromeMessage) {
        const {
            method,
            data,
            headers,
            responseType,
            url,
        } = message.data
        let targetUrl = url
        const init = {
            method,
            headers: {},
        }

        if ((method === 'GET' || method === 'HEAD') && data) {
            targetUrl += '?' + StrUtils.queryToString(data)
        } else if ((method !== 'GET' || method !== 'HEAD') && data) {
            if (data instanceof File) {
                const form = new FormData()
                form.append('image', data)
                Reflect.set(init, 'body', form)
            } else if (data instanceof FormData) {
                Reflect.set(init, 'body', data)
            } else {
                const rowBody = typeof data === 'object' ? JSON.stringify(data) : data
                Reflect.set(init, 'body', rowBody)
            }
        }
        if (headers && typeof headers === 'object') {
            for (const k in headers) {
                Reflect.set(init.headers, k, headers[k])
            }
        }
        const response = {
            readyState: 0,
            responseText: '',
            status: 0,
            response: undefined as any,
            statusText: 'ok'
        }
        try {
            const res = await fetch(targetUrl, init)
            response.readyState = 4
            response.status = res.status
            response.statusText = res.statusText

            if (responseType === 'string' || !responseType) {
                response.responseText = await res.text()
            } else {
                const temp = await res.arrayBuffer()
                response.response = base64.fromByteArray(new Uint8Array(temp))
            }
            return this.getChromeResponse(true, response)
        } catch (error: any) {
            console.log('err', error);
            response.status = 400;
            response.responseText = error.message || 'net error';
            return this.getChromeResponse(true, response)
        }
    }

    async _setValue(message: ChromeMessage) {
        const { key, value, id } = message.data;
        const resourc = new ScriptResource(id);
        await resourc.setCache(key, value);
        return this.getChromeResponse(true, undefined)
    }

    async _setValues(message: ChromeMessage) {
        const caches: ScriptResourceInfo[] = message.data.caches;
        const id = message.data.id;
        const resourc = new ScriptResource(id);
        for (const c of caches) {
            await resourc.setCache(c.key, c.data);
        }
        this.updateScript(id);
        return this.getChromeResponse(true, undefined)
    }

    async _deleteValue(message: ChromeMessage) {
        const { key, id } = message.data;
        const resourc = new ScriptResource(id);
        await resourc.removeCache(key);
        return this.getChromeResponse(true, undefined)
    }

    async _openSettings() {
        await this.bgWorker._openSettings()
        return this.getChromeResponse(true, undefined)
    }

    async _downloadFile(message: ChromeMessage) {
        const { url, filename } = message.data
        chrome.downloads.download({
            url,
            filename: filename || 'myFile',
            saveAs: true
        });
        return this.getChromeResponse(true, undefined)
    }

    async _downloadAll(message: ChromeMessage) {
        const ar: ResourceInfo[] = message.data
        const zip = new JSZip();
        for (let i = 0; i < ar.length; i++) {
            try {
                const res = ar[i];
                const suffix = getResourceSuffix(res)
                const resp = await fetch(res.url)
                const data = resp.blob()
                zip.file(res.name + suffix, data)
            } catch (error) {
                console.log(error)
            }
        }
        const filename = 'resources.zip'
        const blob = await zip.generateAsync({ type: "blob" })
        const file = new File([blob], filename, { type: "application/zip" })
        const url = URL.createObjectURL(file)
        chrome.downloads.download({
            url,
            filename,
            saveAs: true
        });
        return this.getChromeResponse(true, undefined)
    }

    async _desktopCapture(message: ChromeMessage, targetTab: chrome.tabs.Tab) {
        const sources = message.data
        return new Promise((resolve) => {
            try {
                const desktopMediaRequestId = chrome.desktopCapture.chooseDesktopMedia(sources, targetTab, (streamId) => {
                    const res = this.getChromeResponse(true, { streamId, desktopMediaRequestId })
                    resolve(res)
                })
            } catch (error: any) {
                const res = this.getChromeResponse(false, undefined, error.message)
                resolve(res)
            }
        })
    }

    async _cancelDesktopCapture(message: ChromeMessage) {
        const desktopMediaRequestId = message.data
        try {
            chrome.desktopCapture.cancelChooseDesktopMedia(desktopMediaRequestId)
            console.log('cancelDesktopCapture', desktopMediaRequestId)
            return this.getChromeResponse(true, undefined)
        } catch (error: any) {
            return this.getChromeResponse(false, undefined, error.message)
        }
    }

    async _refreshCaches(message: ChromeMessage) {
        const id = message.data;
        await this.updateScript(id);
        const resourc = new ScriptResource(id);
        const caches = await resourc.getCaches();
        return this.getChromeResponse(true, caches);
    }

    async _hello() {
        return this.getChromeResponse(true, 'hello,dog')
    }
}